package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"math/rand"
	"sort"
	"strings"
	"time"

	"github.com/dustinkirkland/golang-petname"
	"github.com/fatih/color"
	"golang.org/x/crypto/ssh"
)

type Hub struct {
	Sessions   map[*Session]struct{}
	Redraw     chan struct{}
	Register   chan *Session
	Unregister chan *Session
}

func NewHub() Hub {
	return Hub{
		Sessions:   make(map[*Session]struct{}),
		Redraw:     make(chan struct{}),
		Register:   make(chan *Session),
		Unregister: make(chan *Session),
	}
}

func (h *Hub) Run(g *Game) {
	for {
		select {
		case <-h.Redraw:
			for s := range h.Sessions {
				go g.Render(s)
			}
		case s := <-h.Register:
			// 隐藏光标， 清屏
			fmt.Fprint(s, "\033[?25l\033[2J")

			h.Sessions[s] = struct{}{}
		case s := <-h.Unregister:
			if _, ok := h.Sessions[s]; ok {
				fmt.Fprint(s, "\r\n\r\n~ End of Line ~ \r\n\r\nRemember to use WASD to move!\r\n\r\n")

				// Unhide the cursor
				fmt.Fprint(s, "\033[?25h")

				delete(h.Sessions, s)

				s.stopAI()
				s.c.Close()
			}
		}
	}
}

type PlayerDirection int

const (
	verticalPlayerSpeed        = 0.004
	horizontalPlayerSpeed      = 0.004
	playerCountScoreMultiplier = 1.25
	playerTimeout              = 100 * time.Second

	playerUpRune    = '⇡'
	playerLeftRune  = '⇠'
	playerDownRune  = '⇣'
	playerRightRune = '⇢'

	playerTrailHorizontal      = '┄'
	playerTrailVertical        = '┆'
	playerTrailLeftCornerUp    = '╭'
	playerTrailLeftCornerDown  = '╰'
	playerTrailRightCornerDown = '╯'
	playerTrailRightCornerUp   = '╮' //= '■'

	PlayerUp PlayerDirection = iota
	PlayerLeft
	PlayerDown
	PlayerRight
)

var playerColors = []color.Attribute{
	red, green, yellow, blue,
	magenta, cyan, white,
}

type Player struct {
	SpiritObject

	s *Session

	Direction PlayerDirection
	Marker    rune
	Color     color.Attribute
	Pos       *Position

	Trail []TrailSegment

	score int

	speedFactor float64
	//毫秒
	speedDuration float64
}

// NewPlayer creates a new player. If color is below 1, a random color is chosen
func NewPlayer(s *Session, worldWidth, worldHeight int,
	color color.Attribute, user string) *Player {

	rand.Seed(time.Now().UnixNano())

	startX := rand.Float64() * float64(worldWidth)
	startY := rand.Float64() * float64(worldHeight)

	if color < 0 {
		color = playerColors[rand.Intn(len(playerColors))]
	}

	return &Player{
		SpiritObject: SpiritObject{
			Name:      user,
			CreatedAt: time.Now(),
		},
		s:         s,
		Marker:    playerDownRune,
		Direction: PlayerDown,
		Color:     color,
		Pos:       &Position{startX, startY},
		score:     0,
	}
}

func (p *Player) addTrailSegment(pos Position, marker rune) {
	segment := TrailSegment{marker, pos, p.Color}
	p.Trail = append([]TrailSegment{segment}, p.Trail...)
}

func (p *Player) popTrailSegment() {
	p.Trail = append(p.Trail[:len(p.Trail)-1])
}

func (p *Player) calculateScore(score int) int {

	//delta float64, playerCount int
	//rawIncrement := (delta * (float64(playerCount-1) * playerCountScoreMultiplier))
	//// Convert millisecond increment to seconds
	//actualIncrement := rawIncrement / 1000

	return p.score + score
}

func (p *Player) speedUp(factor float64, durationSecond float64) {
	p.speedFactor = factor
	p.speedDuration = durationSecond * float64(time.Second) / float64(time.Millisecond)
}

func (p *Player) HandleUp() {
	if p.Direction == PlayerDown {
		return
	}
	p.Direction = PlayerUp
	p.Marker = playerUpRune
	p.s.didAction()
}

func (p *Player) HandleLeft() {
	if p.Direction == PlayerRight {
		return
	}
	p.Direction = PlayerLeft
	p.Marker = playerLeftRune
	p.s.didAction()
}

func (p *Player) HandleDown() {
	if p.Direction == PlayerUp {
		return
	}
	p.Direction = PlayerDown
	p.Marker = playerDownRune
	p.s.didAction()
}

func (p *Player) HandleRight() {
	if p.Direction == PlayerLeft {
		return
	}
	p.Direction = PlayerRight
	p.Marker = playerRightRune
	p.s.didAction()
}

func (p *Player) Score() int {
	return int(p.score)
}

func (p *Player) Speed() int {
	return int(p.speedFactor)
}

func (p *Player) SpeedDuration() int {
	return int(p.speedDuration / 1000)
}

func (p *Player) Update(g *Game, delta float64) {
	startX, startY := p.Pos.RoundX(), p.Pos.RoundY()

	if p.speedDuration > 0 {
		p.speedDuration -= delta
	}
	if p.speedDuration <= 0 {
		p.speedDuration = 0
		p.speedFactor = 1
	}

	switch p.Direction {
	case PlayerUp:
		p.Pos.Y -= verticalPlayerSpeed * delta * p.speedFactor
	case PlayerLeft:
		p.Pos.X -= horizontalPlayerSpeed * delta * p.speedFactor
	case PlayerDown:
		p.Pos.Y += verticalPlayerSpeed * delta * p.speedFactor
	case PlayerRight:
		p.Pos.X += horizontalPlayerSpeed * delta * p.speedFactor
	}

	endX, endY := p.Pos.RoundX(), p.Pos.RoundY()

	// If we moved, add a trail segment.
	if endX != startX || endY != startY {
		var lastSeg *TrailSegment
		var lastSegX, lastSegY int
		if len(p.Trail) > 0 {
			lastSeg = &p.Trail[0]
			lastSegX = lastSeg.Pos.RoundX()
			lastSegY = lastSeg.Pos.RoundY()
		}

		pos := PositionFromInt(startX, startY)

		switch {
		// Handle corners. This took an ungodly amount of time to figure out. Highly
		// recommend you don't touch.
		case lastSeg != nil &&
			(p.Direction == PlayerRight && endX > lastSegX && endY < lastSegY) ||
			(p.Direction == PlayerDown && endX < lastSegX && endY > lastSegY):
			p.addTrailSegment(pos, playerTrailLeftCornerUp)
		case lastSeg != nil &&
			(p.Direction == PlayerUp && endX > lastSegX && endY < lastSegY) ||
			(p.Direction == PlayerLeft && endX < lastSegX && endY > lastSegY):
			p.addTrailSegment(pos, playerTrailRightCornerDown)
		case lastSeg != nil &&
			(p.Direction == PlayerDown && endX > lastSegX && endY > lastSegY) ||
			(p.Direction == PlayerLeft && endX < lastSegX && endY < lastSegY):
			p.addTrailSegment(pos, playerTrailRightCornerUp)
		case lastSeg != nil &&
			(p.Direction == PlayerRight && endX > lastSegX && endY > lastSegY) ||
			(p.Direction == PlayerUp && endX < lastSegX && endY < lastSegY):
			p.addTrailSegment(pos, playerTrailLeftCornerDown)

		// Vertical and horizontal trails
		case endX == startX && endY < startY:
			p.addTrailSegment(pos, playerTrailVertical)
		case endX < startX && endY == startY:
			p.addTrailSegment(pos, playerTrailHorizontal)
		case endX == startX && endY > startY:
			p.addTrailSegment(pos, playerTrailVertical)
		case endX > startX && endY == startY:
			p.addTrailSegment(pos, playerTrailHorizontal)
		}

		//激活AI处理
		p.s.doAI()
	}

	//p.score = p.calculateScore(delta, len(g.players()))

	if len(p.Trail) > p.score+5 {
		p.popTrailSegment()
	}
}

type ByColor []*Player

func (slice ByColor) Len() int {
	return len(slice)
}

func (slice ByColor) Less(i, j int) bool {
	return colorNames[slice[i].Color] < colorNames[slice[j].Color]
}

func (slice ByColor) Swap(i, j int) {
	slice[i], slice[j] = slice[j], slice[i]
}

const (
	gameWidth  = 78
	gameHeight = 22

	keyW = 'w'
	keyA = 'a'
	keyS = 's'
	keyD = 'd'

	keyZ = 'z'
	keyQ = 'q'
	// keyS and keyD are already defined

	keyH = 'h'
	keyJ = 'j'
	keyK = 'k'
	keyL = 'l'

	keyComma = ','
	keyO     = 'o'
	keyE     = 'e'

	keyCtrlC  = 3
	keyEscape = 27
)

type GameManager struct {
	Games         map[string]*Game
	HandleChannel chan ssh.Channel
}

func NewGameManager() *GameManager {
	return &GameManager{
		Games:         map[string]*Game{},
		HandleChannel: make(chan ssh.Channel),
	}
}

// getGameWithAvailability returns a reference to a game with available spots for
// players. If one does not exist, nil is returned.
func (gm *GameManager) getGameWithAvailability() *Game {
	var g *Game

	for _, game := range gm.Games {
		spots := game.AvailableColors()
		if len(spots) > 0 {
			g = game
			break
		}
	}

	return g
}

func (gm *GameManager) SessionCount() int {
	sum := 0
	for _, game := range gm.Games {
		sum += game.SessionCount()
	}
	return sum
}

func (gm *GameManager) GameCount() int {
	return len(gm.Games)
}

func (gm *GameManager) HandleNewChannel(c ssh.Channel, user string) {
	g := gm.getGameWithAvailability()
	if g == nil {
		g = NewGame(gameWidth, gameHeight)
		gm.Games[g.Name] = g

		go g.Run()
	}

	colorOptions := g.AvailableColors()
	finalColor := colorOptions[0]

	// 如果用户名是可用的颜色，就使用该颜色
	user = strings.ToLower(user)
	for _, clr := range colorOptions {
		if strings.ToLower(colorNames[clr]) == user {
			finalColor = clr
			break
		}
	}

	session := NewSession(c, user, g, finalColor)
	g.AddSession(session)

	go func() {
		reader := bufio.NewReader(c)
		for {
			r, _, err := reader.ReadRune()
			if err != nil {
				fmt.Println(err)
				break
			}

			switch r {
			case keyW, keyZ, keyK, keyComma:
				session.Player.HandleUp()
			case keyA, keyQ, keyH:
				session.Player.HandleLeft()
			case keyS, keyJ, keyO:
				session.Player.HandleDown()
			case keyD, keyL, keyE:
				session.Player.HandleRight()
			case keyCtrlC, keyEscape:
				if g.SessionCount() == 1 {
					delete(gm.Games, g.Name)
				}

				g.RemoveSession(session)
			}
		}
	}()
}

type Game struct {
	Name      string
	Redraw    chan struct{}
	HighScore int

	// Top left is 0,0
	ground [][]Tile

	eggSite map[string]*Egg

	hub Hub
}

func NewGame(worldWidth, worldHeight int) *Game {
	g := &Game{
		Name:    petname.Generate(1, ""),
		Redraw:  make(chan struct{}),
		hub:     NewHub(),
		eggSite: make(map[string]*Egg),
	}

	g.initalizeLevel(worldWidth, worldHeight)

	return g
}

func (g *Game) initalizeLevel(width, height int) {
	g.ground = make([][]Tile, width)
	for x := range g.ground {
		g.ground[x] = make([]Tile, height)
	}

	// Default world to grass
	for x := range g.ground {
		for y := range g.ground[x] {
			g.setTileType(Position{float64(x), float64(y)}, TileGrass)
		}
	}
}

func (g *Game) putEgg(x float64, y float64) {
	egg := NewRandomEgg(x, y)
	g.eggSite[egg.Pos.toKey()] = egg
}

func (g *Game) eatEgg(player *Player) {
	//判断吃到 点 了加分
	if player.Pos.RoundX() < gameWidth && player.Pos.RoundY() < gameHeight {
		if _, ok := g.eggSite[player.Pos.toKey()]; ok {
			egg := g.eggSite[player.Pos.toKey()]
			delete(g.eggSite, player.Pos.toKey())

			player.score = player.calculateScore(egg.score())
			player.speedUp(egg.speed())
		}

	}

}

func (g *Game) setTileType(pos Position, tileType TileType) error {
	outOfBoundsErr := "The given %s value (%s) is out of bounds"
	if pos.RoundX() > len(g.ground) || pos.RoundX() < 0 {
		return fmt.Errorf(outOfBoundsErr, "X", pos.X)
	} else if pos.RoundY() > len(g.ground[pos.RoundX()]) || pos.RoundY() < 0 {
		return fmt.Errorf(outOfBoundsErr, "Y", pos.Y)
	}

	g.ground[pos.RoundX()][pos.RoundY()].Type = tileType

	return nil
}

func (g *Game) players() map[*Player]*Session {
	players := make(map[*Player]*Session)

	for session := range g.hub.Sessions {
		players[session.Player] = session
	}

	return players
}

// Characters for rendering
const (
	verticalWall   = '║'
	horizontalWall = '═'
	topLeft        = '╔'
	topRight       = '╗'
	bottomRight    = '╝'
	bottomLeft     = '╚'
)

func (g *Game) fillWorldTile(strWorld [][]string, x, y int, str string) {
	if x >= len(strWorld) || y >= len(strWorld[0]) || x < 0 || y < 0 {
		return
	}
	strWorld[x][y] = str
}

// Warning: this will only work with square worlds
func (g *Game) worldString(s *Session) string {
	worldWidth := len(g.ground)
	worldHeight := len(g.ground[0])

	// Create two dimensional slice of strings to represent the world. It's two
	// characters larger in each direction to accomodate for walls.
	strWorld := make([][]string, worldWidth+2)
	for x := range strWorld {
		strWorld[x] = make([]string, worldHeight+2)
	}

	// Load the walls into the rune slice
	//画墙，横着的部分
	borderColorizer := color.New(colors[s.Player.Color]).SprintFunc()
	for x := 0; x < worldWidth+2; x++ {
		strWorld[x][0] = borderColorizer(string(horizontalWall))
		strWorld[x][worldHeight+1] = borderColorizer(string(horizontalWall))
	}
	//画墙，竖着的部分
	for y := 0; y < worldHeight+2; y++ {
		strWorld[0][y] = borderColorizer(string(verticalWall))
		strWorld[worldWidth+1][y] = borderColorizer(string(verticalWall))
	}

	// Time for the edges!
	//画墙的四个角
	strWorld[0][0] = borderColorizer(string(topLeft))
	strWorld[worldWidth+1][0] = borderColorizer(string(topRight))
	strWorld[worldWidth+1][worldHeight+1] = borderColorizer(string(bottomRight))
	strWorld[0][worldHeight+1] = borderColorizer(string(bottomLeft))

	// Draw the player's score
	scoreStr := fmt.Sprintf(
		" Score: %d : Speed: %d(%d) : YourHighScore: %d : GameHighScore: %d ",
		s.Player.Score(),
		s.Player.Speed(),
		s.Player.SpeedDuration(),
		s.HighScore,
		g.HighScore,
	)
	for i, r := range scoreStr {
		strWorld[3+i][0] = borderColorizer(string(r))
	}

	// Draw the player's color
	colorStr := fmt.Sprintf(" %s ", colorNames[s.Player.Color])
	colorStrColorizer := color.New(s.Player.Color).SprintFunc()
	for i, r := range colorStr {
		charsRemaining := len(colorStr) - i
		strWorld[len(strWorld)-3-charsRemaining][0] = colorStrColorizer(string(r))
	}

	// Draw everyone's scores
	if len(g.players()) > 1 {
		// Sort the players by color name
		players := []*Player{}

		for player := range g.players() {
			if player == s.Player {
				continue
			}

			players = append(players, player)
		}

		sort.Sort(ByColor(players))
		startX := 3

		// Actually draw their scores
		for _, player := range players {
			colorizer := color.New(player.Color).SprintFunc()
			scoreStr := fmt.Sprintf(" %s: %d",
				colorNames[player.Color],
				player.Score(),
			)
			for _, r := range scoreStr {
				strWorld[startX][len(strWorld[0])-1] = colorizer(string(r))
				startX++
			}
		}

		// Add final spacing next to wall
		strWorld[startX][len(strWorld[0])-1] = " "
	} else {
		//warning :=
		//	" Warning: Other Players Must be in This Game for You to Score! "
		//for i, r := range warning {
		//	strWorld[3+i][len(strWorld[0])-1] = borderColorizer(string(r))
		//}
	}

	// Draw the game's name
	nameStr := fmt.Sprintf(" %s ", g.Name)
	for i, r := range nameStr {
		charsRemaining := len(nameStr) - i
		strWorld[len(strWorld)-3-charsRemaining][len(strWorld[0])-1] =
			borderColorizer(string(r))
	}

	// Load the ground into the string slice
	for x := 0; x < worldWidth; x++ {
		for y := 0; y < worldHeight; y++ {
			tile := g.ground[x][y]

			switch tile.Type {
			case TileGrass:
				strWorld[x+1][y+1] = string(grass)
			default:

			}
		}
	}

	//遍历 eggSite
	for _, v := range g.eggSite {
		x, y := v.Pos.RoundX()+1, v.Pos.RoundY()+1
		strWorld[x][y] = v.render()
	}

	// Load the players into the rune slice
	for player := range g.players() {

		pos := player.Pos
		colorizer := color.New(player.Color).SprintFunc()
		g.fillWorldTile(strWorld, pos.RoundX()+1, pos.RoundY()+1, colorizer(string(player.Marker)))
		//strWorld[pos.RoundX()+1][pos.RoundY()+1] = colorizer(string(player.Marker))

		// Load the player's trail into the rune slice
		for _, segment := range player.Trail {
			x, y := segment.Pos.RoundX()+1, segment.Pos.RoundY()+1
			colorizer := color.New(segment.Color).SprintFunc()
			//strWorld[x][y] = colorizer(string(segment.Marker))
			g.fillWorldTile(strWorld, x, y, colorizer(string(segment.Marker)))
		}
	}

	// Convert the rune slice to a string
	buffer := bytes.NewBuffer(make([]byte, 0, worldWidth*worldHeight*2))
	for y := 0; y < len(strWorld[0]); y++ {
		for x := 0; x < len(strWorld); x++ {
			buffer.WriteString(strWorld[x][y])
		}

		// Don't add an extra newline if we're on the last iteration
		if y != len(strWorld[0])-1 {
			buffer.WriteString("\r\n")
		}
	}

	return buffer.String()
}

func (g *Game) WorldWidth() int {
	return len(g.ground)
}

func (g *Game) WorldHeight() int {
	return len(g.ground[0])
}

func (g *Game) AvailableColors() []color.Attribute {
	usedColors := map[color.Attribute]bool{}
	for _, color := range playerColors {
		usedColors[color] = false
	}

	for player := range g.players() {
		usedColors[player.Color] = true
	}

	availableColors := []color.Attribute{}
	for color, used := range usedColors {
		if !used {
			availableColors = append(availableColors, color)
		}
	}

	return availableColors
}

func (g *Game) SessionCount() int {
	return len(g.hub.Sessions)
}

func (g *Game) Run() {
	// Proxy g.Redraw's channel to g.hub.Redraw
	go func() {
		for {
			g.hub.Redraw <- <-g.Redraw
		}
	}()

	// Run game loop
	go func() {
		var lastUpdate time.Time
		//lastUpdate=time.Now()

		c := time.Tick(time.Second / 60)
		for now := range c {
			g.Update(float64(now.Sub(lastUpdate)) / float64(time.Millisecond))

			lastUpdate = now
		}
	}()

	// Redraw regularly.
	//
	// TODO: Implement diffing and only redraw when needed
	go func() {
		c := time.Tick(time.Second / 10)
		for range c {
			g.Redraw <- struct{}{}
		}
	}()

	g.hub.Run(g)
}

func (g *Game) putEgg4Dead(player *Player) {
	//死了刷几个点
	if player.score > 0 {
		var c = player.score / 2
		for i := 0; i < c; i++ {
			g.putEgg(player.Trail[2*i].Pos.X, player.Trail[2*i].Pos.Y)
		}
	}
}

// Update is the main game logic loop. Delta is the time since the last update
// in milliseconds.
func (g *Game) Update(delta float64) {
	// We'll use this to make a set of all of the coordinates that are occupied by
	// trails
	trailCoordMap := make(map[string]*TrailSegment)

	// Update player data
	for player, session := range g.players() {
		player.Update(g, delta)

		// Update session high score, if applicable
		if player.Score() > session.HighScore {
			session.HighScore = player.Score()
		}

		// Update global high score, if applicable
		if player.Score() > g.HighScore {
			g.HighScore = player.Score()
		}

		// Restart the player if they're out of bounds
		pos := player.Pos
		if pos.RoundX() < 0 || pos.RoundX() >= len(g.ground) ||
			pos.RoundY() < 0 || pos.RoundY() >= len(g.ground[0]) {
			g.putEgg4Dead(player)
			session.StartOver(g)
		}

		// Kick the player if they've timed out
		if time.Now().Sub(session.LastAction) > playerTimeout {
			fmt.Fprint(session, "\r\n\r\nYou were terminated due to inactivity\r\n")
			g.RemoveSession(session)
			return
		}

		// range gives copies, but we need a reference in the trailCoordMap so we
		// can modify the Color value if there is a collision, so iterate by index
		// instead.
		for i := range player.Trail {
			seg := &player.Trail[i]
			trailCoordMap[seg.Pos.toKey()] = seg
		}
	}

	// Check if any players collide with a trail and restart them if so
	for player, session := range g.players() {
		g.eatEgg(player)
		if segment, collided := trailCoordMap[player.Pos.toKey()]; collided {
			segment.Color = player.Color
			g.putEgg4Dead(player)
			session.StartOver(g)
		}
	}

	// 刷新 点
	if len(g.eggSite) < len(g.players())+4 {
		g.putEgg(-1, -1)
	}

}

func (g *Game) Render(s *Session) {
	worldStr := g.worldString(s)

	var b bytes.Buffer
	b.WriteString("\033[H") //\033[2J  //把清屏指令拿掉了。界面就不会闪了
	b.WriteString(worldStr)

	// Send over the rendered world
	io.Copy(s, &b)
}

func (g *Game) AddSession(s *Session) {
	g.hub.Register <- s
}

func (g *Game) RemoveSession(s *Session) {
	g.hub.Unregister <- s
}

type Session struct {
	c ssh.Channel

	LastAction time.Time
	HighScore  int
	Player     *Player
	ai         *AIPlayer
	useAI      bool
}

func NewSession(c ssh.Channel, user string, g *Game,
	color color.Attribute) *Session {

	s := Session{
		c:          c,
		LastAction: time.Now(),
		useAI:      user == "ai",
	}

	s.newGame(g, color, user)

	return &s
}

func (s *Session) newGame(g *Game, color color.Attribute, user string) {
	s.Player = NewPlayer(s, g.WorldWidth(), g.WorldHeight(), color, user)

	if s.useAI {
		s.ai = NewAI(s.Player, g)
		go s.ai.Run()
	}

}

func (s *Session) doAI() {
	if s.useAI {
		s.ai.ch <- 1
	}
}

func (s *Session) stopAI() {
	if s.useAI {
		s.ai.stop <- 1
	}
}

func (s *Session) didAction() {
	s.LastAction = time.Now()
}

func (s *Session) StartOver(g *Game) {
	s.stopAI()
	s.newGame(g, s.Player.Color, s.Player.Name)
}

func (s *Session) Read(p []byte) (int, error) {
	return s.c.Read(p)
}

func (s *Session) Write(p []byte) (int, error) {
	return s.c.Write(p)
}
